var plot = (function() {

    ///////////////////////////////////////////////////////////////////////////////
    //
    //  Waveform plotting
    //
    ///////////////////////////////////////////////////////////////////////////////

    // return [first tick value >= vmin, tick interval]
    function tick_interval(vmin,vmax,nticks) {
        var log_vtick = Math.log((vmax - vmin)/Math.max(1,nticks))/Math.LN10;
        var exponent = Math.floor(log_vtick);
        var mantissa = Math.pow(10,log_vtick - exponent);  // between 1 and 10

        // pick tick interval based on 1,2,5 progression of scope divisions
        var interval;
        if (mantissa >= 4.99) interval = 5;
        else if (mantissa >= 1.99) interval = 2;
        else interval = 1;
        interval *= Math.pow(10,exponent);   // scale correctly

        var vstart = Math.floor(vmin/interval) * interval;
        if (vstart < vmin) vstart += interval;
        return [vstart,interval];
    }

    var normal_style = 'rgb(0,0,0)'; // default drawing color
    var background_style = 'rgb(238,238,238)';
    var element_style = 'rgb(255,255,255)';
    var grid_style = "rgb(220,220,220)";
    var graph_font = '8pt sans-serif';
    var graph_legend_font = '10pt sans-serif';

    // dataseries is an array of objects that value the following attributes:
    //   xvalues: array of xcoords
    //   yvalues: array of ycoords
    //   name: signal name to use in legend (optional)
    //   color: color to use when drawing graph
    //   xunits: string for labeling xvalues (optional)
    //   yunits: string for labeling yvalues (optional - if omitted assumed to be bits)
    //   xlabel: string for labeling x axis (optional)
    //   ylabel: string for labeling y axis (optional)
    function graph(dataseries) {
        // create container
        var container = $('<div class="plot-container"></div>');
        container[0].dataseries = dataseries;
        dataseries.container = container[0];

        // add toolbar
        var toolbar = $('<div class="plot-toolbar"></div>');
        var zoom = $('<img class="plot-tool" id="zoom">').attr('src',zoom_icon);
        var zoomin = $('<img class="plot-tool plot-tool-enabled" id="zoomin">').attr('src',zoomin_icon);
        var zoomout = $('<img class="plot-tool" id="zoomout">').attr('src',zoomout_icon);
        var zoomsel = $('<img class="plot-tool" id="zoomsel">').attr('src',zoomsel_icon);
        toolbar.append(zoom,zoomin,zoomout,zoomsel);

        if (dataseries.add_plot) {
            toolbar.append('<div class="jade-tool-spacer"></div>');
            var add_plot = $('<span class="plot-tool plot-tool-enabled">add plot</span>');
            toolbar.append(add_plot);

            add_plot.on('click',function () {
                // pass callback to user's add_plot function
                // they should call it once for each new dataset
                dataseries.add_plot(function (dataset) {
                    process_dataset(dataset);
                    dataseries.push(dataset);
                    do_plot(container[0], container.width(), container.height());
                });
            });
        }

        container.append(toolbar);

        // set up scroll bar
        container.append('<div class="plot-scrollbar-wrapper"><div class="plot-scrollbar"><div class="plot-scrollbar-thumb"></div></div></div>');


        // handlers for zoom tools
        zoom.on('click',function () {
            if (zoom.hasClass('plot-tool-enabled')) {
                dataseries.xstart = dataseries.xmin;
                dataseries.xend = dataseries.xmax;
                do_plot(container[0],container.width(),container.height());
            }
        });

        zoomin.on('click',function () {
            if (zoomin.hasClass('plot-tool-enabled')) {
                dataseries.xend -= (dataseries.xend - dataseries.xstart)/2;
                do_plot(container[0],container.width(),container.height());
            }
        });

        zoomout.on('click',function () {
            if (zoomout.hasClass('plot-tool-enabled')) {
                dataseries.xend += (dataseries.xend - dataseries.xstart);
                if (dataseries.xend > dataseries.xmax) {
                    dataseries.xstart = Math.max(dataseries.xmin, dataseries.xstart-(dataseries.xend-dataseries.xmax));
                    dataseries.xend = dataseries.xmax;
                }
                do_plot(container[0],container.width(),container.height());
            }
        });

        zoomsel.on('click',function () {
            if (zoomsel.hasClass('plot-tool-enabled') && dataseries.sel0 && dataseries.sel1) {
                var x0 = dataseries[0].datax(dataseries.sel0);
                var x1 = dataseries[0].datax(dataseries.sel1);
                dataseries.xstart = Math.min(x0,x1);
                dataseries.xend = Math.max(x0,x1);
                dataseries.sel0 = undefined;   // all done with region!
                dataseries.sel1 = undefined;
                do_plot(container[0],container.width(),container.height());
            }
        });

        function process_dataset(dataset) {
            dataset.dataseries = dataseries;   // remember our parent

            // remember min and max xvalues across all the datasets
            var xvalues = dataset.xvalues;
            if (dataseries.xmin === undefined || xvalues[0] < dataseries.xmin)
                dataseries.xmin = xvalues[0];
            if (dataseries.xmax === undefined || xvalues[xvalues.length - 1] > dataseries.xmax)
                dataseries.xmax = xvalues[xvalues.length - 1];

            // anotate each dataset with ymin and ymax
            var ymin = 0, ymax = 1;  // defaults chosen for logical (bit) values
            if (dataset.yunits) {
                // if this is a real quantity (voltage, current), find max and min
                $.each(dataset.yvalues,function (dindex,y) {
                    if (dindex == 0 || y < ymin) ymin = y;
                    if (dindex == 0 || y > ymax) ymax = y;
                });
            }
            // expand y range by 10% to leave a margin above and below the waveform
            if (ymin == ymax) {
                // deal with degenerate case...
                if (ymin === 0) { ymin = -0.5; ymax = 0.5; }
                else {
                    ymin = ymin > 0 ? 0.9 * ymin : 1.1 * ymin;
                    ymax = ymax > 0 ? 1.1 * ymax : 0.9 * ymax;
                }
            } else {
                var yextra = 0.2 * (ymax - ymin);
                ymin -= yextra;
                ymax += yextra;
            }
            dataset.ymin = ymin;
            dataset.ymax = ymax;

            // set up canvas for DOM, also one for background image
            dataset.canvas = $('<canvas class="plot-canvas"></canvas>');
            dataset.canvas[0].plot_dataset = dataset;  // for event processing

            // handle click in close box
            dataset.canvas.on('click',function (event) {
                var pos = dataset.canvas.offset();
                var gx = event.pageX - pos.left;
                var gy = event.pageY - pos.top;

                if (gx >= 5.5 && gx <= 15.5 && gy >= 5.5 && gy <= 15.5) {
                    // remove dataset from DOM and dataseries
                    dataseries.splice(dataseries.indexOf(dataset),1);
                    dataset.canvas.remove();

                    // replot remaining datasets
                    do_plot(container[0],container.width(),container.height());
                }
            });

            // dragging in plot creates a selection region
            dataset.canvas.on('mousedown',function (event) {
                var pos = dataset.canvas.offset();
                var gx = event.pageX - pos.left;
                var gy = event.pageY - pos.top;

                // see if mouse is over plot region
                if (gx >= dataset.left && gx <= dataset.left + dataset.wplot &&
                    gy >= dataset.top && gy <= dataset.top + dataset.hplot) {
                    dataseries.sel0 = dataseries.cursor;   // remember start of region
                    dataseries.sel1 = undefined;
                    dataseries.sel = true;
                }

                $(document).on('mouseup',function () {
                    $(document).unbind('mouseup');
                    dataseries.sel = undefined;      // we're done defining region
                    graph_redraw(dataseries);
                });
            });

            // display vertical cursor for measurements on curve
            dataset.canvas.on('mousemove',function (event) {
                var pos = dataset.canvas.offset();
                var gx = event.pageX - pos.left;
                var gy = event.pageY - pos.top;

                // see if mouse is over plot region
                if (gx >= dataset.left && gx <= dataset.left + dataset.wplot &&
                    gy >= dataset.top && gy <= dataset.top + dataset.hplot) {
                    dataseries.cursor = Math.floor(gx) + 0.5;
                    if (dataseries.sel) dataseries.sel1 = dataseries.cursor;
                } else dataseries.cursor = undefined;

                graph_redraw(dataseries);
            });

            dataset.bg_image = $('<canvas></canvas>');

            // handle retina devices properly
            var context = dataset.canvas[0].getContext('2d');
            var devicePixelRatio = window.devicePixelRatio || 1;
            var backingStoreRatio = context.webkitBackingStorePixelRatio ||
                    context.mozBackingStorePixelRatio ||
                    context.msBackingStorePixelRatio ||
                    context.oBackingStorePixelRatio ||
                    context.backingStorePixelRatio || 1;
            dataset.pixelRatio = devicePixelRatio / backingStoreRatio;

            dataset.canvas.insertBefore(container.find('.plot-scrollbar-wrapper'));
        }

        // compute value bounds, set up canvas
        $.each(dataseries,function (index,dataset) { process_dataset(dataset); });
        dataseries.xstart = dataseries.xmin;   // set up initial xaxis bounds
        dataseries.xend = dataseries.xmax;
        dataseries.cursor = undefined;    // x-coord of mouse cursor over plot

        // set up handlers for dragging scrollbar thumb
        var thumb = container.find('.plot-scrollbar-thumb');
        var scrollbar = container.find('.plot-scrollbar');
        thumb.on('mousedown',function (event) {
            var thumb_value = parseInt(thumb.css('margin-left'));
            var thumb_max_value = scrollbar.width() - thumb.width();
            var thumb_dx = (dataseries.xmax - dataseries.xmin)/scrollbar.width();
            var thumb_width = thumb.width() * thumb_dx;
            var mx = event.pageX;

            $(document).on('mousemove',function (event) {
                var dx = event.pageX - mx;
                var value = Math.min(thumb_max_value,Math.max(0,thumb_value + dx));
                thumb.css('margin-left',value);
                dataseries.xstart = dataseries.xmin + value*thumb_dx;
                dataseries.xend = dataseries.xstart + thumb_width;
                do_plot(container[0],container.width(),container.height());
            });

            $(document).on('mouseup',function (event) {
                $(document).unbind('mousemove');
                $(document).unbind('mouseup');
            });
        });

        // set up resize handler
        container[0].resize = do_plot;

        // the initial plot
        do_plot(container[0], 400, 300);

        return container[0];
    }

    function do_plot(container,w,h) {
        var dataseries = container.dataseries;
        var plot_h = Math.floor((h - 30 - 20)/dataseries.length);  // height of each plot

        $(container).width(w);
        $(container).height(h);

        // set dimensions of each canvas, figure out consistent margins
        var left_margin = 55.5;
        var right_margin = 19.5;
        var top_margin = 5.5;
        var bottom_margin = 15.5;
        $.each(dataseries,function (index,dataset) {
            //dataset.canvas.css('top',index*plot_h + 25);  // position canvas in container
            dataset.canvas.width(w);
            dataset.canvas.height(plot_h);
            dataset.canvas[0].width = w*dataset.pixelRatio;
            dataset.canvas[0].height = plot_h*dataset.pixelRatio;
            // after changing dimension, have to reset context 
            dataset.canvas[0].getContext('2d').scale(dataset.pixelRatio,dataset.pixelRatio);

            dataset.bg_image[0].width = w*dataset.pixelRatio;
            dataset.bg_image[0].height = plot_h*dataset.pixelRatio;
            dataset.bg_image[0].getContext('2d').scale(dataset.pixelRatio,dataset.pixelRatio);

            if (dataset.ylabel !== undefined) left_margin = 70.5;
            if (dataset.xlabel !== undefined) bottom_margin = 35.5;
        });

        $(container).find('.plot-scrollbar').css('margin-left',left_margin).css('margin-right',right_margin);

        // now that dimensions are set, do the plots
        var wplot = w - left_margin - right_margin;
        var hplot = plot_h - top_margin - bottom_margin;
        var xscale = (dataseries.xend - dataseries.xstart)/wplot;
        $.each(dataseries,function (index,dataset) {
            // set up coordinate transforms
            var yscale = (dataset.ymax - dataset.ymin)/hplot;
            dataset.plotx = function(datax) {
                return (datax - dataseries.xstart)/xscale + left_margin;
            };
            dataset.ploty = function(datay) {
                return top_margin + (hplot - (datay - dataset.ymin)/yscale);
            };
            dataset.datax = function(plotx) {
                return (plotx - left_margin)*xscale + dataseries.xstart;
            };

            // compute info for drawing grids -- shoot for a grid line every 100 pixels
            dataset.xtick = tick_interval(dataseries.xstart,dataseries.xend,wplot/100);
            dataset.xtick.push(dataseries.xend);  // when to stop drawing x grid
            dataset.ytick = tick_interval(dataset.ymin,dataset.ymax,hplot/100);

            // save margin and size info
            dataset.left = left_margin;
            dataset.top = top_margin;
            dataset.wplot = wplot;
            dataset.hplot = hplot;

            dataset.color = '#268bd2';  // fixed color for now

            // draw the plot
            dataset_plot(dataset);
        });
        graph_redraw(dataseries);

        // set up toolbar
        var maxzoom = (dataseries.xstart == dataseries.xmin && dataseries.xend == dataseries.xmax);
        $(container).find('#zoom').toggleClass('plot-tool-enabled',!maxzoom);
        $(container).find('#zoomout').toggleClass('plot-tool-enabled',!maxzoom);

        // set up scrollbar
        $(container).find('.plot-scrollbar-thumb').toggle(!maxzoom);
        if (!maxzoom) {
            var thumb = $(container).find('.plot-scrollbar-thumb');
            var wthumb = ((dataseries.xend - dataseries.xstart)/(dataseries.xmax - dataseries.xmin))*wplot;
            var xthumb = ((dataseries.xstart - dataseries.xmin)/(dataseries.xmax - dataseries.xmin))*wplot;
            thumb.css('width',wthumb);
            thumb.css('margin-left',xthumb);
        }
    }

    // redraw the plot for a particular dataset by filling in background image
    function dataset_plot(dataset) {
        var xstart = dataset.dataseries.xstart;
        var xend = dataset.dataseries.xend;
        var tick_length = 5;

        // start by painting an opaque background for the plot itself
        var c = dataset.bg_image[0].getContext('2d');

        c.clearRect(0, 0, dataset.bg_image[0].width, dataset.bg_image[0].height);
        //c.fillStyle = background_style;
        //c.fillRect(0, 0, dataset.bg_image[0].width, dataset.bg_image[0].height);

        c.fillStyle = element_style;
        c.fillRect(dataset.left, dataset.top, dataset.wplot, dataset.hplot);

        // draw xgrid and tick labels
        c.strokeStyle = grid_style;
        c.fillStyle = normal_style;
        c.font = graph_font;
        c.textAlign = 'center';
        c.textBaseline = 'top';
        var t,temp;
        var xunits = dataset.xunits || '';
        for (t = dataset.xtick[0]; t < dataset.xtick[2]; t += dataset.xtick[1]) {
            temp = Math.floor(dataset.plotx(t)) + 0.5;

            c.beginPath();
            c.moveTo(temp,dataset.top); c.lineTo(temp,dataset.top + dataset.hplot);
            c.stroke();
            c.fillText(jade.utils.engineering_notation(t, 2)+xunits, temp, dataset.top + dataset.hplot);
        }

        if (dataset.yunits) {
            // draw ygrid and tick labels
            c.textAlign = 'right';
            c.textBaseline = 'middle';
            for (t = dataset.ytick[0]; t < dataset.ymax; t += dataset.ytick[1]) {
                temp = Math.floor(dataset.ploty(t)) + 0.5;

                c.beginPath();
                c.moveTo(dataset.left,temp); c.lineTo(dataset.left + dataset.wplot,temp);
                c.stroke();
                c.fillText(jade.utils.engineering_notation(t, 2)+dataset.yunits,dataset.left-2,temp);
            }
            c.stroke();
        }

        // draw axis labels
        c.font = graph_legend_font;
        if (dataset.xlabel) {
            c.textAlign = 'center';
            c.textBaseline = 'bottom';
            c.fillText(dataset.xlabel, dataset.left + dataset.wplot/2, dataset.bg_image[0].height-5);
        }
        if (dataset.ylabel) {
            c.save();
            c.textAlign = 'center';
            c.textBaseline = 'top';
            c.translate(10, dataset.top + dataset.hplot/2);
            c.rotate(-Math.PI / 2);
            c.fillText(dataset.ylabel, 0, 0);
            c.restore();
        }

        c.save();
        c.beginPath();
        c.rect(dataset.left,dataset.top,dataset.wplot,dataset.hplot);
        c.clip();   // clip waveform plot to waveform region of canvas
        var i = search(dataset.xvalues,xstart);  // quickly find first index
        if (dataset.yunits) {
            // plot the analog waveform
            c.strokeStyle = dataset.color;
            c.lineWidth = 2;
            var xv = dataset.xvalues[i];
            var x = dataset.plotx(xv);
            var y = dataset.ploty(dataset.yvalues[i]);
            c.beginPath();
            c.moveTo(x, y);
            while (xv < xend) {
                i += 1;
                xv = dataset.xvalues[i];
                if (xv === undefined) break;
                var nx = dataset.plotx(xv);
                var ny = dataset.ploty(dataset.yvalues[i]);
                c.lineTo(nx, ny);
                x = nx;
                y = ny;
                if (i % 100 == 99) {
                    // too many lineTo's cause canvas to break
                    c.stroke();
                    c.beginPath();
                    c.moveTo(x, y);
                }
            }
            c.stroke();
        } else {
            // plot the digital waveform
        }
        c.restore();

        // add plot border last so it's on top
        c.lineWidth = 1;
        c.strokeStyle = normal_style;
        c.strokeRect(dataset.left, dataset.top, dataset.wplot, dataset.hplot);

        // add close box
        c.strokeRect(5.5,5.5,10,10);
        c.beginPath();
        c.moveTo(7.5,7.5); c.lineTo(13.5,13.5);
        c.moveTo(13.5,7.5); c.lineTo(7.5,13.5);
        c.stroke();

        // add legend: translucent background with 5px padding, 15x15 color key, signal label
        var left = dataset.left + 5;
        var top = dataset.top + 5;
        var w = c.measureText(dataset.name).width;
        c.globalAlpha = 0.7;
        c.fillStyle = element_style;
        c.fillRect(left, top, w + 30, 25);
        c.globalAlpha = 1.0;

        c.fillStyle = dataset.color;
        c.fillRect(left+5, top+5, 15, 15);
        c.strokeRect(left+5, top+5, 15, 15);

        c.fillStyle = normal_style;
        c.textAlign = 'left';
        c.textBaseline = 'bottom';
        c.fillText(dataset.name, left + 25, top+20);

        // remember where legend ends so we can add cursor readout later
        dataset.legend_right = left + 25 + w;
        dataset.legend_top = top;
    }

    function graph_redraw(dataseries) {
        $(dataseries.container).find('#zoomsel').toggleClass('plot-tool-enabled',dataseries.sel0!==undefined && dataseries.sel1!==undefined);

        // redraw each plot with cursor overlay
        $.each(dataseries,function(index,dataset) {
            var c = dataset.canvas[0].getContext('2d');
            c.clearRect(0, 0, dataset.canvas.width(), dataset.canvas.height());
            c.drawImage(dataset.bg_image[0], 0, 0, dataset.canvas.width(), dataset.canvas.height());

            // show selection region, if any
            if (dataseries.sel0 && dataseries.sel1) {
                c.fillStyle = 'rgba(207,191,194,0.4)';
                var xsel = Math.min(dataseries.sel0,dataseries.sel1);
                var wsel = Math.abs(dataseries.sel0 - dataseries.sel1);
                c.fillRect(xsel,dataset.top,wsel,dataset.hplot);

                c.strokeStyle = 'rgba(207,191,194,0.8)';
                c.lineWidth = 1;
                c.beginPath();
                c.moveTo(xsel,dataset.top); c.lineTo(xsel,dataset.top+dataset.hplot);
                c.moveTo(xsel+wsel,dataset.top); c.lineTo(xsel+wsel,dataset.top+dataset.hplot);
                c.stroke();
            }

            if (dataseries.cursor !== undefined) {
                // overlay vertical plot cursor
                c.lineWidth = 1;
                c.strokeStyle = normal_style;
                c.beginPath();
                c.moveTo(dataseries.cursor,dataset.top);
                c.lineTo(dataseries.cursor,dataset.top + dataset.hplot);
                c.stroke();

                // draw fiducial at intersector of cursor and curve
                var x = dataset.datax(dataseries.cursor);  // convert cursor coord to x value
                var i = search(dataset.xvalues,x);  // quickly find first index
                // interpolate cursor's intersection with curve
                var x1 = dataset.xvalues[i];
                var y1 = dataset.yvalues[i];
                var x2 = dataset.xvalues[i+1] || x1;
                var y2 = dataset.yvalues[i+1] || y1;
                var y = y1;
                if (x1 != x2) y = y1 + ((x - x1)/(x2-x1))*(y2 - y1);

                var gx = dataset.plotx(x);
                var gy = dataset.ploty(y);
                c.beginPath();
                c.arc(gx,gy,5,0,2*Math.PI);
                c.stroke();

                // add x-axis label
                var label = jade.utils.engineering_notation(x,1);
                if (dataset.xunits) label += dataset.xunits;
                c.font = graph_font;
                c.textAlign = 'center';
                c.textBaseline = 'top';
                c.fillStyle = background_style;
                c.fillText('\u2588\u2588\u2588\u2588\u2588', dataseries.cursor, dataset.top + dataset.hplot);
                c.fillStyle = normal_style;
                c.fillText(label, dataseries.cursor, dataset.top + dataset.hplot);

                // now add label
                if (dataset.yunits) {
                    label = '='+jade.utils.engineering_notation(y,1) + dataset.yunits;
                    c.font = graph_legend_font;

                    // translucent background so graph doesn't obscure label
                    var w = c.measureText(label).width;
                    c.fillStyle = element_style;
                    c.globalAlpha = 0.7;
                    c.fillRect(dataset.legend_right,dataset.legend_top,w+5,25);

                    // now plot the label itself
                    c.textAlign = 'left';
                    c.textBaseline = 'bottom';
                    c.fillStyle = normal_style;
                    c.globalAlpha = 1.0;
                    c.fillText(label,dataset.legend_right,dataset.legend_top+20);
                }
            }
        });
    }

    // find largest index in array such that array[index] <= val
    // return 0 if all array elements are >= val
    // assumes array contents are in increasing order
    // uses a binary search
    function search(array, val) {
        var start = 0;
        var end = array.length-1;
        var index;
        while (start < end) {
            index = (start + end) >> 1;   // "middle" index
            if (index == start) index = end;
            if (array[index] <= val) start = index;
            else end = index - 1;
        }
        return start;
    }

    var zoom_icon = '';

    var zoomin_icon = '';

    var zoomout_icon = '';

    var zoomsel_icon = '';

    // module exports
    return {graph: graph,tick_interval: tick_interval};
}());
